今天,來優化爬蟲的速度。
回顧一下,我們的程式執行了以下步驟:
我們先來記錄一下各步驟執行的時間。
// 在 Top Level 增加記錄用時的變數
let DownloadT = 0,
ParseT = 0,
OrganizeT = 0,
SaveT = 0;
我們用 Date.now()
來記錄各時間點的時間,然後計算差值來獲取所花時間。
Download: 542.958s, Parse: 69.241s, Organize: 0.005s, Save: 0.036s
總共花了 612.24 秒,其中花最多時間的是下載網頁 (88.7%),再來是解析網頁 (11.3%),合併數據及儲存數據的部分所花時間則皆不足 0.1%。
爬取 1047 個頁面花費 543 秒,平均一個網頁請求需約 500 毫秒。用瀏覽器的 console 調查了一下,發現單一網頁文件大小只有約 15 kB 爬取所有頁面也才需要約 15 MB 的大小。我們可以推斷,請求延遲阻塞了許多時間。
在處理請求延遲的問題上,我們可以使用並發來減少其對總時間之影響,當然也得確定並發數量不會過大造成伺服器負擔。
我們的方法是:第一次請求時紀錄總共有多少頁,接著直接依照紀錄的執行請求,而不從當前頁面推斷下一頁,這樣就不會因為要知道前一頁才能處理下一頁而造成組塞了。當然為了預防在爬取時有新的文章出現,我們會在每次爬取時確定總頁數有沒有增加,動態的提升總頁數紀錄。
我們在執行前先設定最大並發上限:
// 在 Top Level 且在 main() 執行前
let available = 12; // 最大並行上限設為 12
const finished = [];
function isAvailable() {
return new Promise((resolve) => {
if (available > 0) {
available--;
resolve();
} else finished.push(resolve);
});
}
我們會使用 isAvailable
來阻塞避免超過最大上限,當已有最大上限之行程在執行時,則會等待直至現有行程完成而出現空缺。
同時,我們將 crawler
中的部分程式移出至新的 task
函式:
async function task(url) {
console.log(`Crawling Page: ${url}`);
const result = new Map();
const DT = Date.now();
const html = await fetch(url).then((res) => res.text());
DownloadT += Date.now() - DT;
const PT = Date.now();
const dom = new JSDOM(html);
const document = dom.window.document;
// 以 CSS Selector 尋找總頁數
const pageCount = parseInt(document.querySelector(".pagination > li:nth-last-child(2)").textContent);
const articles = document.querySelectorAll("li.ir-list");
for (const article of articles) {
const parsed = parseArticle(article);
result.set(parsed.link, parsed);
}
ParseT += Date.now() - PT;
if (finished.length > 0) {
// 釋出空間讓其他任務可以執行
const resolve = finished.shift();
resolve();
}
return { result, pageCount };
}
所以現在我們的 crawler
函式只剩下:
async function crawler(startURL) {
const result = new Map();
// 先爬取第一頁
const firstPage = await task(startURL);
firstPage.result.forEach((val, key) => result.set(key, val));
// 總頁數,注意:我們用 let 而非 const
let total = firstPage.pageCount;
// 紀錄任務用,最後使用 Promise.all 來確認所有請求皆已完成
const tasks = [];
for (let i = 2; i <= total; i++) {
// 用來在已達最大上限時阻塞
await isAvailable();
// 把任務放入任務集合
tasks.push(
task(startURL + "?page=" + i).then((page) => {
page.result.forEach((val, key) => result.set(key, val));
// 如果發現總頁數增加了,則更新總頁數
if (page.pageCount > total) total = page.pageCount;
})
);
// 當到最後一頁時,等待所有任務都完成再跳出,或有新的總頁數則繼續
if(i === total) await Promise.all(tasks);
}
// 回傳陣列型態的 result
return [...result.values()];
}
其他的部分,我們不需要做任何更動,執行看看吧!
Running...
Crawled 10563 Articles in 130.73s
Download: 1370.683s, Parse: 53.579s, Organize: 0.006s, Save: 0.166s
我們用 130.7 秒爬了 10563 篇文章,平均一秒爬取 80 篇文章,也就是 8 頁,速度是未優化前的 3.2 倍。
等等,Download 時間為什麼有 1370 秒?因為 Download 跟 Parse 都是並行的,所以計時上會有重疊的狀況。
並行 (concurrency) 的好處是可以讓 CPU 的使用效率更高,而無須花太多時間因等待而閒置計算資源。
資料爬下來之後就拿來分析並做成下面那個東西吧!
明天就來做每日鐵人賽熱門 Top 10!
以 9/24 20:00 ~ 9/25 20:00 文章觀看數增加值排名
+953
Day 1 無限手套 AWS 版:掌控一切的 5 + 1 雲端必學主題
+803
Day 3 雲端四大平台比較:AWS . GCP . Azure . Alibaba
+801
Day 2 AWS 是什麼?又為何企業這麼需要 AWS 人才?
+777
Day 4 網路寶石:AWS VPC Region/AZ vs VPC/Subnet 關係介紹
+750
Day 5 網路寶石:AWS VPC 架構 Routes & Security (上)
+727
Day 6 網路寶石:AWS VPC 架構 Routes & Security (下)
+725
Day 8 網路寶石:【Lab】VPC外網 Public Subnet to the Internet (IGW) (下)
+724
Day 9 運算寶石:EC2 重點架構
+717
Day 10 運算寶石:EC2 儲存資源 Instance Store vs Elastic Block Storage (EBS)
+714
Day 7 網路寶石:【Lab】VPC外網 Public Subnet to the Internet (IGW) (上)
看來今天 AWS 文章觀看數增加速度收斂一點了